iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0
JavaScript

Vue.js學習中的細節陷阱:30天自我學習指南系列 第 18

Day 18: JavaScript 工廠函式 和 類別(class)

  • 分享至 

  • xImage
  •  

終於來到鐵人賽的後半段囉~給自己一點鼓勵!

前半段都在理解和複習Vue的一些使用上的觀念和陷阱,接下來希望能複習一些基本JavaScript觀念,讓我們可以慢慢推進到更進階的JavaScript設計模式等。

之前已經有處理一些重複性邏輯時,會使用所謂的組合式函式(composable)來達成複用,其中衍生的觀念式是從JavaScript工廠模式(Factory Pattern),是一個很基礎但常出現的設計模式,只是不太常察覺。

順便熟悉一下另一個類別建構子(class),兩者同樣能達成共用物件或擴充的目的。今天就稍微複習一下,兩者沒有絕對好或壞,針對專案需求可以選擇覺得合適的設計方案就行。~

今日學習重點

  1. 認識 JavaScript 類別建構子工廠函式的差別
  2. 閉包(closure)觀念再複習
  3. 兩者如何封裝私有變數(private)
  4. 兩者如何擴充延展和繼承(extend and inheritance),差異性在哪裡?
  5. 兩者開發選擇上的差異總結

認識 JavaScript 類別建構子工廠函式的差別

類別建構子

類別建構子(class) 是 ECMAScript 2015 (ES6) 規範中的 JavaScript 新增的特性。類別提供了一種就像是物件導向程式設計(OOP)方法,對於使用 Java 或 C++ 等語言的開發人員來說,會感到很熟悉,雖然本質上JavaSript 是沒有class系統,是用原型繼承(prototype inheritance)來模擬。

使用特點:

  • 使用class關鍵字來定義
  • 利用constructor用於初始化新實例的內部變數
  • 實例方法添加到原型(prototype)
  • 需要透過this來指向建構子的變數(非同步稍微注意指向問題)
  • 用new關鍵字實例化
  • 支援透過extend關鍵字來實現繼承
class Rectangle {
  // 建構函式定義初始化物件綁定資料
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // 內部定義方法
  getArea() {
    return this.height * this.width;
  }
}

const rect = new Rectangle(10, 20);
console.log(rect.getArea()); // 200

工廠函式

工廠函式(factory function)是一個在ES6之前還沒有類別出現時常見的設計模式,是將物件的創建過程封裝在函式中,並透過該函式來生成新的物件,當然也可以只返回一個特定變數,相較於類別在回傳值上有更高靈活性。

我們可以用工廠函式於創建對象的過程中不直接使用new 關鍵字來實例化對象,可以通過函數來生成和返回對象。

使用特點:

  • 透過返回值回傳一個定義實例屬性或方法的對象
  • 不使用new關鍵字創建,採用熟悉的函式呼叫(invoke)
  • 彼此繼承的話需要調用其它物件方法(Object assign,展開運算子等)
function createRectangle(height, width) {
  return {
    height,
    width, 

    getArea() {
      return this.height * this.width;
    }
  }
}

const rect = createRectangle(10, 20);
console.log(rect.getArea()); // 200

閉包(closure)觀念再強化

先看一個簡單的應用,為什麼呼叫 makeAdding 函式並輸入參數5,最終可以得到返回值7呢?

function makeAdding (firstNumber) {
  const first = firstNumber;
  return function resulting (secondNumber) {
    const second = secondNumber;
    return first + second;
  }
}

const add5 = makeAdding(5);
console.log(add5(2)) // logs 7 
  • 函數makeAdding接受一個參數 ,firstNumber宣告一個first值為 的常數firstNumber,並傳回另一個函數。

  • 當參數2傳遞給傳回的函式add5時,它會傳回將先前傳遞的數字5與現在傳遞的數字2相加的結果,也就是7,在JavaScript中函式會形成閉包(closure)。

  • 閉包是指函式和其被宣告時所在的周圍狀態(也稱為語彙環境lexiccal scope)的組合,這個周圍狀態包含了函式建立時在作用域內的所有局部變數

當 makeAdding 函式執行(execution phase)時,add5 是指向makeAdding函式,其中包含了變數 first,JS為了能夠正確運行並捕捉對應的變數資料來進行運算,透過宣告時期(creation phase)已經建立好的語彙環境,去搜尋變數 first被賦予的值,也就是5。

閉包的關鍵 : 允許我們將資料與函式關聯,並在封閉函式外的作用域(scope)去獲取這些資料。


封裝私有變數

工廠函式如何封裝私有變數

工廠函式的一大優點是能夠利用閉包(closure)來創建「私有」變數和函式,這些變數和函式只能在工廠函式內部訪問,而無法從外部直接存取。

可以很直觀透過返回值去控制模組需要公開的方法和屬性,這使得模組的使用者只能使用經過我們設計過的API介面(interface),增強了程式碼的可靠性和易用性。

function createUser(name) {
  // 私有變數
  let privateId = Math.random().toString(36).substr(2, 9);

  // 私有函式
  function generateGreeting() {
    return `Hello, ${name}! Your ID is ${privateId}.`;
  }

  // 公開的變數和函式
  const discordName = "@" + name;

  return {
    name,
    discordName,
    getGreeting: generateGreeting, // 公開方法訪問私有函式
    getId: () => privateId, // 公開方法訪問私有變數
  };
}

// 使用工廠函式
const user = createUser("Alice");


// 無法直接訪問私有變數和函式
console.log(user.privateId); // undefined
console.log(user.generateGreeting); // undefined

類別封裝私有變數

當然你會想說物件建構子做不到實現私有屬性和方法嗎,其實好像可以, ES2022 引入了私有屬性語法(使用 # 前綴),讓我們可以在 class 中定義真正的私有屬性和方法:

class User {
  // 私有變數使用 # 前綴
  #privateId;
  #privateMethod() {
    console.log("This is a private method.");
  }

  constructor(name) {
    this.name = name;
    this.discordName = "@" + name;
    this.#privateId = Math.random().toString(36).substr(2, 9);
  }

  // 公開方法可以訪問私有屬性和方法
  getId() {
    return this.#privateId;
  }

  publicMethod() {
    console.log("This is a public method.");
    this.#privateMethod(); // 呼叫私有方法
  }
}

const user = new User("Alice");

// 嘗試訪問私有屬性或方法會失敗
console.log(user.#privateId); // SyntaxError: Private field '#privateId' must be declared in an enclosing class
console.log(user.#privateMethod); // SyntaxError: Private field '#privateMethod' must b

延展和繼承(extend and inheritance)

類別擴展和繼承

類別建構子使用extend滿方便的,在新的物件實例使用起來也滿直觀~

但目前看到比較嚴重的缺點是方法屬性同名的話,後代會有改寫並覆蓋的情況,在實務上應該比較不希望這種狀況產生,如果後面的高階模組一直蓋掉低階模組,使用起來好像會混淆。

需要做一層屬性定義defineProperty,不然會後代新方法覆蓋掉原本父設計好函式的風險。

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}
// 凍結 speak 方法,不可以覆寫
Object.defineProperty(Animal.prototype, 'speak', {
  writable: false,
  configurable: false
});

class Dog extends Animal {
  constructor(name) {
    super(name);
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog(‘Rex‘);
dog.speak(); // Rex barks.

工廠函式擴充

工廠函式上面有介紹過會返回一個新的物件,而不需要使用 new 關鍵字

如果希望做到可以有類似class父子類繼承作用的,需要使用 Object.assign展開運算子(spread operator)將原本的函式中的方法複製到新物件,我記得之前物件操作方法章節有提到。

不過個人感覺使用上不像類別extend,有較清楚的物件繼承上下關係,反而比較像混合模式(Mixin pattern),將各種屬性集中到同一物件,因為同樣會有作用域命名(named scope)上的問題,合併順序就滿重要,因為後面同樣的屬性會覆蓋掉前面重複名稱。

補充: JavaScript Mixin 混合模式


// 製作一個基本動物函式
const animalMethods = {
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
};

const runnerMethods = {
  run() {
    console.log(`${this.name} is running.`);
  }
};

// 工廠函式繼承多個方法
function createAnimal(name) {
  return {
    name,
    ...animalMethods,
    ...runnerMethods // 使用展開運算子將多個方法合併
  };
}

// 創建一隻動物
const myAnimal = createAnimal('Cheetah');
myAnimal.speak(); // Cheetah makes a noise.
myAnimal.run();   // Cheetah is running.

和類別(class)繼承共用方法的差異

const pobby = createAnimal(Cheetah);

// 檢查 pobby 的原形
console.log(Object.getPrototypeOf(pobby)); // 輸出: Object.prototype
console.log(pobby instanceof createAnimal); // 輸出: false
  • 工廠函式返回的是新物件-沒有和原本函式綁定原型鏈

工廠函式返回的物件沒有利用 JavaScript 的原型鏈來共享方法,每個物件都是獨立的,方法是直接附加在物件上的。這些方法並不共享,而是每個物件有自己的一份副本。這和 class 不同,class 可以利用原型來共享方法。


總結: 工廠函式 vs 類別建構子差異

方法共享原型鏈

類別class中,當你定義方法時,這些方法會被添加到類別的原型鏈上共享。也就是說,所有由這 class 創建的物件實例都會共享同一個方法,而不是每個物件都有自己的副本,不過在目前設備硬體都很充分情況下,大部分開發效能差異性並不大。

物件導向OOP概念

使用類別class的方式更符合熟悉物件導向概念的開發者,尤其當設計關係上出現,需要多層次繼承時會更具可讀性和可維護性

特性 工廠函式 類別class
方法位置 每個實例都有自己的方法副本 方法位於原型鏈上,所有實例共享同一個方法
記憶體效率 每次創建都複製方法,可能會浪費記憶體(很大量的話) 方法共享,記憶體使用更有效
代碼風格與結構 更具靈活性,可自己定義返回的物件 適合更複雜的物件結構,符合 OOP 模式
擴展性 靈活,但擴展較複雜,比較沒上下關係,偏混合式(mixin)風格 直接使用 extends 繼承,明確層級關係,可讀性會比較高

學習資源

  1. https://www.bomberbot.com/javascript/class-vs-factory-function-exploring-the-way-forward/
  2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties (ES 2020 private class)
  3. https://www.patterns.dev/vanilla/factory-pattern (JS pattern)
  4. https://www.theodinproject.com/lessons/node-path-javascript-factory-functions-and-the-module-pattern (這篇最讚,必讀)

上一篇
Day 17: Vue-非受控元件認識、列表渲染(v-for)的陷阱
下一篇
Day 19: Vue - 組合式邏輯概念 (Vue composable concept)
系列文
Vue.js學習中的細節陷阱:30天自我學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言